#include <glib.h>
#include <glib/gstdio.h>
#include <libxml/SAX2.h>
#include <libxml/parser.h>
#include <string.h>

#include "mainview-parser.h"
#include "desktopClasses.h"

#define MAINXML "main.xml"
#define OPTIONSXML "options.xml"

#define PARSER_OPTION  XML_PARSE_NOBLANKS|XML_PARSE_NOENT|XML_PARSE_NOCDATA|XML_PARSE_RECOVER
#define DEFAULT_LOCALE "en"
#define STRINGS "strings"
extern struct Element array[];

static gboolean set_js_properties(js_engine *engine, desktop_element *widget,
				  struct property_parser *parser);
extern JSClass viewClass;
extern JSClass eventClass;
extern JSClass elementsClass;
extern JSClass buttonClass;
extern JSClass checkboxClass;
extern JSClass objectClass;
extern JSClass contentareaClass;
extern JSClass divClass;
extern JSClass editClass;
extern JSClass imgClass;
extern JSClass labelClass;
extern JSClass aClass;
extern JSClass progressbarClass;
extern JSClass radioClass;
extern JSClass listboxClass;
extern JSClass listitemClass;

extern JSFunctionSpec view_methods[];
extern JSFunctionSpec elements_methods[];

extern struct property_parser view_parser[];
extern struct property_parser basicElement_parser[];
extern struct property_parser button_parser[];
extern struct property_parser checkbox_parser[];
extern struct property_parser object_parser[];
extern struct property_parser contentarea_parser[];
extern struct property_parser div_parser[];
extern struct property_parser edit_parser[];
extern struct property_parser img_parser[];
extern struct property_parser label_parser[];
extern struct property_parser a_parser[];
extern struct property_parser progressbar_parser[];
extern struct property_parser scrollbar_parser[];
extern struct property_parser listbox_parser[];
extern struct property_parser listitem_parser[];
extern struct property_parser options_parser[];
extern struct property_parser radio_parser[];

void *has_setter(struct property_parser *parser, const gchar *key) {
    g_return_val_if_fail(parser, NULL);
    while (parser->name) {
        if (g_strcasecmp(key, parser->name) == 0) {
            return parser->setter;
        }
        parser++;
    }
    return NULL;
}

static gboolean set_js_properties(js_engine *engine, desktop_element *widget,
				  struct property_parser *parser) {
    GList *keys = g_hash_table_get_keys(widget->attrs);
    jsval innerText;
    int i;
    for (i = 0; i < g_list_length(keys); i++) {
        gchar *key = g_list_nth_data(keys, i);

        JSBool (*setter)(js_engine *engine, desktop_element *widget,
			 const gchar *key) = has_setter(parser, key);
        if (setter) {
            setter(engine, widget, key);
        } else {
	    set_property_string_value(engine, widget, key);
        }
    }
    innerText = STRING_TO_JSVAL(JS_NewStringCopyZ(engine->cx, desktop_element_get_text(widget)));
    JS_SetProperty(engine->cx, widget->jsobj, "innerText", &innerText);
    return TRUE;
}

JSBool set_property_string_value(js_engine *engine, desktop_element *widget,
				 const gchar *key) {
    jsval value;
    gchar *value_str = g_hash_table_lookup(widget->attrs, key);
    JSString *string = JS_NewStringCopyZ(engine->cx, value_str);
    value = STRING_TO_JSVAL(string);
    return (JS_SetProperty(engine->cx, widget->jsobj, key, &value));
}

JSBool set_property_integer_value(js_engine *engine, desktop_element *widget,
				  const gchar *key) {
    jsval value;
    gint64 value_int = g_ascii_strtoll(g_hash_table_lookup(widget->attrs, key), 
				       NULL, 10);
    g_return_val_if_fail(JSVAL_INT_MIN < value_int && value_int < JSVAL_INT_MAX, JS_FALSE);
    value = INT_TO_JSVAL(value_int);
    return (JS_SetProperty(engine->cx, widget->jsobj, key, &value));
}

JSBool set_property_float_value(js_engine *engine, desktop_element *widget,
				const gchar *key) {
    jsval value;
    gdouble value_double = g_ascii_strtod(g_hash_table_lookup(widget->attrs, key), NULL);
    value = DOUBLE_TO_JSVAL(value_double);
    return (JS_SetProperty(engine->cx, widget->jsobj, key, &value));
}

JSBool set_property_boolean_value(js_engine *engine, desktop_element *widget,
				  const gchar *key) {
    jsval value;
    gchar *value_str = g_hash_table_lookup(widget->attrs, key);
    
    if (g_ascii_strcasecmp(value_str, "true") == 0){
        value = JSVAL_TRUE;
    }else if (g_ascii_strcasecmp(value_str, "false") == 0){
        value = JSVAL_FALSE;
    }else
        value = JSVAL_TRUE;
    return (JS_SetProperty(engine->cx, widget->jsobj, key, &value));
}

JSBool set_property_function_value(js_engine *engine, desktop_element *widget,
				   const gchar *key) {
    jsval handler;
    gchar *handler_str = g_hash_table_lookup(widget->attrs, key);
    gchar **split = g_strsplit(handler_str, "(", 2);
    JS_GetProperty(engine->cx, engine->view, split[0], &handler);
    if (JS_ObjectIsFunction(engine->cx, JSVAL_TO_OBJECT(handler))){
        return (JS_SetProperty(engine->cx, widget->jsobj, key, &handler));
    }
    return JS_FALSE;
}

JSBool set_property_stub(js_engine *engine, desktop_element *widget,
			 const gchar *key) {
    return JS_TRUE;
}

static gboolean set_js_object(js_engine *engine, desktop_element *new) {
    JSObject *newObject = NULL, *children = NULL;
    JSClass *class = NULL;
    struct property_parser *parser = NULL;
    int i;

    if(g_strcasecmp(new->type, NAME_BUTTON) == 0){
        class = &buttonClass;
        parser = button_parser;
    }else if(g_strcasecmp(new->type, NAME_CHECKBOX) == 0){
        class = &checkboxClass;
        parser = checkbox_parser;
    }else if(g_strcasecmp(new->type, NAME_OBJECT) == 0){
        class = &objectClass;
        parser = object_parser;
    }else if(g_strcasecmp(new->type, NAME_CONTENTAREA) == 0){
        class = &contentareaClass;
	parser = contentarea_parser;
    }else if(g_strcasecmp(new->type, NAME_DIV) == 0){
        class = &divClass;
	parser = div_parser;
    }else if(g_strcasecmp(new->type, NAME_EDIT) == 0){
        class = &editClass;
	parser = edit_parser;
    }else if(g_strcasecmp(new->type, NAME_IMG) == 0){
        class = &imgClass;
	parser = img_parser;
    }else if(g_strcasecmp(new->type, NAME_LABEL) == 0){
        class = &labelClass;
	parser = label_parser;
    }else if(g_strcasecmp(new->type, NAME_A) == 0){
        class = &aClass;
	parser = a_parser;
    }else if(g_strcasecmp(new->type, NAME_PROGRESSBAR) == 0){
        class = &progressbarClass;
	parser = progressbar_parser;
    }else if(g_strcasecmp(new->type, NAME_RADIO) == 0){
        class = &radioClass;
	parser = radio_parser;
    }else if(g_strcasecmp(new->type, NAME_LISTBOX) == 0){
        class = &listboxClass;
	parser = listbox_parser;
    }else if(g_strcasecmp(new->type, NAME_LISTITEM) == 0){
        class = &listitemClass;
	parser = listitem_parser;
    }else if(g_strcasecmp(new->type, NAME_SCRIPT) == 0){
	return TRUE;
    }else{
	g_warning("%s is not implemented.",new->type);
	return TRUE;
    }

    if (new->name)
        newObject = JS_DefineObject(engine->cx, engine->view, new->name, class, NULL, 0);
    else
        newObject = JS_ConstructObject(engine->cx, class, NULL, engine->view);
    
    new->jsobj = newObject;
    JS_SetPrivate(engine->cx, new->jsobj, new);
    children = JS_DefineObject(engine->cx, new->jsobj, "children", &elementsClass, engine->glob, 0);
    JS_DefineFunctions(engine->cx, children, elements_methods);
    JS_SetPrivate(engine->cx, children, new);
    set_js_properties(engine, new, parser);

    for(i = 0; i < g_list_length(new->children); i++){
	desktop_element *child =  g_list_nth_data(new->children, i);
	set_js_object(engine, child);
    }
    return FALSE;
}

gchar *truncate_text(const gchar *original) {
    if (original) {
        gchar *truncated= NULL;
        gchar *reversed = g_utf8_strreverse(original, -1);
        gchar *ptr = reversed;
        while (TRUE) {
            if (*ptr == '\0')
                break;
            if (g_ascii_isspace(*ptr))
                ptr++;
            else
                break;
        }
        truncated = g_utf8_strreverse(ptr, -1);
        g_free(reversed);
        return truncated;
    } else {
        return NULL;
    }
}

desktop_element *parse_xmlNode(js_engine *engine, xmlNodePtr ptr) {
    desktop_element *new;
    xmlAttr *attrs;
    xmlNode *children;
    g_return_val_if_fail(ptr, NULL);

    new = desktop_element_new((gchar *)ptr->name, engine);
    attrs = ptr->properties;
    while (attrs) {
        gchar *key = (gchar *)attrs->name;
        gchar *value = (gchar *)attrs->children->content;
        if (g_ascii_strcasecmp(key, "name") == 0){
            desktop_element_set_name(new, value);
        }else{
            desktop_element_set_attr(new, key, value);
	}
        attrs = attrs->next;
    }

    children = ptr->children;
    while (children) {
        if (children->type == XML_ELEMENT_NODE) {
	    desktop_element *child_widget = parse_xmlNode(engine, children);
	    desktop_element_add_child(new, child_widget);
	    child_widget->parent = new;
        } else if (children->type == XML_TEXT_NODE) {
            gchar *text;
            text = truncate_text((const gchar *)children->content);
            desktop_element_set_text(new, text);
            g_free(text);
        }
        children = children->next;
    }
    return new;
}

static gchar *get_stringsxml_path(void);
static GHashTable *get_strings_table(js_engine *engine,
				     const gchar *stringsxml_path);

gboolean mainview_set_js_object(js_engine *engine, desktop_element *element)
{
    JSObject *obj, *children;
    jsval children_val;
    int i;

    g_return_val_if_fail(g_strcasecmp(element->type, "view") == 0, FALSE);
    obj = JS_DefineObject(engine->cx, engine->glob, "view", &viewClass, engine->glob, 0);
    element->jsobj = obj;
    engine->view = obj;
    JS_DefineFunctions(engine->cx, element->jsobj, view_methods);
    JS_DefineObject(engine->cx, element->jsobj, "event", &eventClass, engine->glob, 0);
    gadgetClass_init(engine, element->jsobj);

    children = JS_ConstructObject(engine->cx, &elementsClass, engine->glob, NULL);
    children_val = OBJECT_TO_JSVAL(children);
    JS_SetProperty(engine->cx, element->jsobj, "children", &children_val);
    JS_SetPrivate(engine->cx, children, element);

    JS_SetContextPrivate(engine->cx, engine->view);
    JS_SetPrivate(engine->cx, engine->view, element);

    for(i = 0; i < g_list_length(element->children); i++){
	desktop_element *child = g_list_nth_data(element->children, i);
	if(strcmp(child->type, "script") == 0){
	    const gchar *file = g_hash_table_lookup(child->attrs, "src");
	    js_engine_load_script_file(engine, engine->view, file);
	}
    }
    set_js_properties(engine, element, view_parser);
    for(i = 0; i < g_list_length(element->children); i++){
	desktop_element *child = g_list_nth_data(element->children, i);
	set_js_object(engine, child);
    }
    return TRUE;
}

gboolean optionsview_set_js_object(js_engine *engine, desktop_element *element)
{
    JSObject *obj, *children;
    jsval children_val;
    int i;

    g_return_val_if_fail(g_strcasecmp(element->type, "view") == 0, FALSE);
    obj = JS_NewObject(engine->cx, &viewClass, NULL, engine->glob);
    JS_AddRoot(engine->cx, &obj);
    element->jsobj = obj;
    engine->view = obj;
    JS_DefineFunctions(engine->cx, element->jsobj, view_methods);

    children = JS_ConstructObject(engine->cx, &elementsClass, engine->glob, NULL);
    children_val = OBJECT_TO_JSVAL(children);
    JS_SetProperty(engine->cx, element->jsobj, "children", &children_val);
    JS_SetPrivate(engine->cx, children, element);

    for(i = 0; i < g_list_length(element->children); i++){
	desktop_element *child = g_list_nth_data(element->children, i);
	if(strcmp(child->type, "script") == 0){
	    const gchar *file = g_hash_table_lookup(child->attrs, "src");
	    js_engine_load_script_file(engine, engine->view, file);
	}
    }

    set_js_properties(engine, element, view_parser);

    for(i = 0; i < g_list_length(element->children); i++){
	desktop_element *child = g_list_nth_data(element->children, i);
	set_js_object(engine, child);
    }
    return TRUE;
}

desktop_element *load_main_xml(js_engine *engine, const gchar *xmlfile, GHashTable *table) {
    GString *cleaned_viewxml = g_string_new(NULL);
    GString *entity = NULL;
    gboolean isEntity = FALSE;
    GError *err = NULL;
    GIOChannel *fd;
    xmlDoc *viewDoc;
    xmlNodePtr ptr;
    desktop_element *root;

    g_return_val_if_fail(engine, FALSE);
    g_return_val_if_fail(xmlfile, FALSE);
    g_return_val_if_fail(table, FALSE);

    fd = g_io_channel_new_file(xmlfile, "r", &err);

    if (err) {
        g_error_free(err);
        return NULL;
    }
    g_io_channel_set_encoding(fd, NULL, &err);
    while (TRUE) {
        gchar ch;
        gsize bytes_read;
        GError *err= NULL;
        GIOStatus status = g_io_channel_read_chars(fd, &ch, 1, &bytes_read,
						   &err);
        if (err) {
            g_error_free(err);
            g_io_channel_close(fd);
            g_message("err");
            return NULL;
        }
        if (status == G_IO_STATUS_EOF)
            break;
        if (ch == '&') {
            isEntity = TRUE;
            entity = g_string_new(NULL);
        } else if (ch == ';') {
            if (isEntity == TRUE) {
                gchar *subst = g_hash_table_lookup(table, entity->str);
                g_string_append(cleaned_viewxml, subst);
                isEntity = FALSE;
                g_string_free(entity, TRUE);
                entity = NULL;
            } else {
                g_string_append_c(cleaned_viewxml, ch);
            }
        } else {
            if (isEntity == FALSE) {
                g_string_append_c(cleaned_viewxml, ch);
            } else {
                g_string_append_c(entity, ch);
            }
        }
    }
    viewDoc = xmlReadMemory(cleaned_viewxml->str, cleaned_viewxml->len, NULL,
			    NULL, 0);
    g_return_val_if_fail(viewDoc, NULL);
    ptr = xmlDocGetRootElement(viewDoc);

    root = parse_xmlNode(engine, ptr);
    xmlFreeDoc(viewDoc);
    
    return root;
}

desktop_element *options_dialog_parse(js_engine *engine)
{
    gchar *stringsxmlDir = get_stringsxml_path();
    GHashTable *stringTable = get_strings_table(engine, stringsxmlDir);
    desktop_element *options;
    g_free(stringsxmlDir);
    
    if(g_file_test(OPTIONSXML, G_FILE_TEST_EXISTS)){
	options = load_main_xml(engine, OPTIONSXML, stringTable);
	optionsview_set_js_object(engine, options);
	return options;
    }else{
	g_warning(_("options.xml: No such file."));
	return NULL;
    }
}

desktop_element *mainview_parse(js_engine *engine) {
    gchar *stringsxmlDir = get_stringsxml_path();
    GHashTable *stringTable = get_strings_table(engine, stringsxmlDir);
    g_free(stringsxmlDir);
    return load_main_xml(engine, MAINXML, stringTable);
}

static GHashTable *get_strings_table(js_engine *engine, const gchar *stringsxml) {
    xmlDoc *doc = NULL;
    xmlNodePtr root;
    xmlNode *ptr;
    GHashTable *table = 
	g_hash_table_new_full(g_str_hash, g_str_equal,
			      g_free, g_free);
    gchar *stringsxml_path = 
	g_strconcat(stringsxml, "/strings.xml", NULL);
    jsval stringsArray;
    JSObject *strings_obj;

    doc = xmlReadFile(stringsxml_path, NULL, PARSER_OPTION);
    g_free(stringsxml_path);
    if(doc == NULL)
        return table;

    root = xmlDocGetRootElement(doc);
    g_return_val_if_fail(root, NULL);

    strings_obj = JS_NewArrayObject(engine->cx, 0, NULL);
    g_return_val_if_fail(JS_IsArrayObject(engine->cx, strings_obj), NULL);

    stringsArray = OBJECT_TO_JSVAL(strings_obj);
    JS_SetProperty(engine->cx, engine->glob, STRINGS, &stringsArray);
    ptr = root->children;
    while (ptr) {
        if(ptr->type == XML_ELEMENT_NODE) {
            JSString *string = JS_NewStringCopyZ(engine->cx, (gchar *)ptr->children->content);
            jsval val = STRING_TO_JSVAL(string);
            g_hash_table_insert(table, xmlStrdup(ptr->name),
				xmlStrdup(ptr->children->content));
            g_return_val_if_fail(JS_SetProperty(engine->cx, engine->glob, (gchar *)ptr->name,
						&val), NULL);
            g_return_val_if_fail(JS_SetProperty(engine->cx, strings_obj, (gchar *)ptr->name,
						&val), NULL);
            
        }
        ptr = ptr->next;
    }
    xmlFreeDoc(doc);
    return table;
}

struct LCIDPair {
    const gchar *lang;
    const gchar *id;
};

struct LCIDPair table[] = { 
    { "de", "1031" }, 
    { "en", "1033" },
    { "es", "1034" }, 
    { "fr", "1036" }, 
    { "it", "1040" }, 
    { "ja", "1041" },
    { "ko", "1042" },
    { "nl", "1043" }, 
    { "pl", "1045" }, 
    { "pt", "2070" },
    { "ru", "1049" },
    { "sv", "1053" },
    { "zh_CN", "2052" },
    { "zh_TW", "1028" }, 
    { NULL, NULL } 
};

const gchar *get_lcid(const gchar *lang) {
    struct LCIDPair *ptr = table;
    while (ptr->lang) {
        if (g_strcasecmp(lang, ptr->lang) == 0)
            return ptr->id;
        ptr++;
    }
    return NULL;
}

static gchar *get_stringsxml_path(void) {
    const gchar * const*langs = g_get_language_names();
    while (*langs) {
        if (g_file_test(*langs, G_FILE_TEST_EXISTS)) {
            return g_strdup(*langs);
        }
        langs++;
    }
    langs = g_get_language_names();
    while (*langs) {
        if (g_file_test(get_lcid(*langs), G_FILE_TEST_EXISTS)) {
            return g_strdup(get_lcid(*langs));
        }
        langs++;
    }

    if (g_file_test(DEFAULT_LOCALE, G_FILE_TEST_EXISTS))
        return g_strdup(DEFAULT_LOCALE);
    else if (g_file_test(get_lcid(DEFAULT_LOCALE), G_FILE_TEST_EXISTS))
        return g_strdup(get_lcid(DEFAULT_LOCALE));

    return NULL;
}
